-- Copyright (c) 2025 Huawei Technologies Co., Ltd.
-- openUBMC is licensed under Mulan PSL v2.
-- You can use this software according to the terms and conditions of the Mulan PSL v2.
-- You may obtain a copy of Mulan PSL v2 at:
--         http://license.coscl.org.cn/MulanPSL2
-- THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
-- EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
-- MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
-- See the Mulan PSL v2 for more details.

local skynet = require 'skynet'
local log = require 'mc.logging'
local class = require 'mc.class'
local singleton = require 'mc.singleton'
local definition = require 'canbus.definition'

local canbus_dispatcher = class()

function canbus_dispatcher:ctor()
    self.message_maps = {}
    self.trans_frames = {}
    self.message_handlers = {}
    self.message_dispatched = {}
    log:notice('canbus message dispatcher is created')
end

function canbus_dispatcher:validate_message(channel, msg)
    local can_id = definition.can_id_pattern:unpack(msg:sub(1, definition.can_id_offset))
    if not can_id then
        log:notice('message is invalid and ignore it')
        return false
    end

    if not self.message_maps[can_id.trans_id] then
        self.message_maps[can_id.trans_id] = {}
    end

    self.message_maps[can_id.trans_id][can_id.frame_id] = msg:sub(definition.can_id_offset + 1)

    if can_id.last == 1 then
        -- record last frame id of a transmission
        self.trans_frames[can_id.trans_id] = {
            last_frame = can_id.frame_id,
            can_id = can_id,
            channel = channel,
            timeout_ticks = 0
        }
    end
    return true
end

function canbus_dispatcher:dispatch_monitor()
    skynet.fork_once(function()
        while true do
            skynet.sleep(definition.can_transmission_monitor.INTERVAL)

            -- start monitor all transmitted frames
            local remove_maps = {}
            for k, v in pairs(self.trans_frames) do
                -- drop the incomplete message transmission
                if v.timeout_ticks > definition.can_transmission_monitor.TICKS then
                    table.insert(remove_maps, k)
                    log:warn('canbus transmission [%s] some frame is timeout and drop this message', k)
                    goto continue
                end

                -- check message is completed or not, which have last frame
                for i = 1, v.last_frame do
                    if not self.message_maps[k] or not self.message_maps[k][i] then
                        v.timeout_ticks = v.timeout_ticks + 1
                        goto continue
                    end
                end

                -- move completed message to dispatch queue
                self.message_dispatched[k] = {msgs = self.message_maps[k], can_id = v.can_id, channel = v.channel}
                table.insert(remove_maps, k)
                ::continue::
            end

            -- remove deprecated or dispatched message centralized
            for _, v in ipairs(remove_maps) do
                self.trans_frames[v] = nil
                self.message_maps[v] = nil
            end
        end
    end)
end

function canbus_dispatcher:dispatch()
    skynet.fork_once(function()
        while true do
            if not next(self.message_dispatched) then
                skynet.sleep(definition.can_dispatch.INTERVAL)
                goto task_continue
            end

            -- start monitor all dispatching messages
            local remove_maps = {}
            for k, v in pairs(self.message_dispatched) do
                local cmd = string.byte(v.msgs[1]:sub(1, 2)) -- 1st byte is fixed to cmd
                if not cmd then
                    log:warn('message [cmd: %s] is invalid, drop this message', cmd)
                    table.insert(remove_maps, k)
                    goto dispatch_continue
                end

                local handler = self.message_handlers[cmd]
                if not handler then
                    log:warn('message [cmd: 0x%02X] has no handler, drop this message', cmd)
                    table.insert(remove_maps, k)
                    goto dispatch_continue
                end

                local ok, ret = pcall(handler, v.can_id, v.channel, v.msgs)
                if not ok then
                    log:error('dispatch and handle message failed, error: %s', ret)
                end

                -- the dispatched message will be removed regardless of the handle result
                table.insert(remove_maps, k)
                ::dispatch_continue::
            end

            -- remove dispatched message centralized
            for _, v in ipairs(remove_maps) do
                self.message_dispatched[v] = nil
            end

            ::task_continue::
        end
    end)
end

function canbus_dispatcher:initialize()
    -- register a skynet protocol named CAN-FD for receiving and distributing specified canfd message
    skynet.register_protocol({
        name = 'CAN-FD',
        id = definition.can_protocol_id,
        pack = skynet.pack,
        unpack = skynet.unpack
    })
    skynet.dispatch('CAN-FD', function(_, _, cmd, data, ...)
        if cmd ~= 'recv' then
            log:debug('canbus message [cmd: %s] is unknown', cmd)
            return
        end

        -- NOTE: here only validate and distribute message to slots [and channel is useless]
        skynet.ret(skynet.pack(self:validate_message(data.channel, data.msg)))
    end)

    -- start a task to monitor each transmission of message with timeout
    self:dispatch_monitor()

    -- start a task to dispatch message
    self:dispatch()
    log:notice('canbus message dispatcher is initialized')
end

function canbus_dispatcher:register_cmd_handler(cmd, handler)
    local found = false
    for _, v in pairs(definition.can_cmds) do
        if cmd == v then
            found = true
            break
        end
    end

    if not found or not handler then
        log:error('canbus cmd [%s] or handler [%s] is invalid', cmd, handler)
        return
    end

    if self.message_handlers[cmd] then
        log:warn('canbus cmd [%s] has handler and ignore registry', cmd)
        return
    end

    self.message_handlers[cmd] = handler
    log:notice('canbus cmd [%s] handler is registered completed', cmd)
end

return singleton(canbus_dispatcher)